feat(sexpr): add linked-via operator + document the linked-* family (#190)#265
Merged
Conversation
…#190) Issue #190 reported that the s-expr DSL has no obvious way to express "this artifact has at least one outbound link of type T". `linked-by` already does that semantically, but the issue author (and others) read it as "linked-by some inbound thing", so the discoverable spelling is missing — and the four linked-* operators have no semantic table in the docs. Changes: - New `(linked-via "T")` operator in `rivet-core/src/sexpr_eval.rs`, lowering to `Expr::LinkedBy(T, Wildcard)` so the evaluator and the `links-count` complement remain a single code path. Arity = 1. - Error-hint generated from the `HEADS` array (the lowerer's source of truth) instead of a hand-maintained string, so the next operator added cannot leave the hint stale. Hint now enumerates the linked-* family explicitly rather than the opaque `linked-*` glob. - `docs/getting-started.md`: 7-row semantic table covering `linked-via` / `linked-by` / `linked-from` / `linked-to` / `links-count` with direction + filter axis + truth condition, plus three gap-hunt examples (`(not (linked-via "T"))`, etc.). - 6 new unit tests in `sexpr_eval.rs` covering: outbound-present, outbound-absent, `(not …)` finds gap, arity guard (0 / 1 / 2 args), `linked-via "T"` ≡ `linked-by "T" _` equivalence, and a regression test on the parse-error hint enumerating the family. - 2 new integration tests in `sexpr_doc_examples.rs` covering the new doc table + gap-hunt example. - `docs_listed_predicates_all_parse_as_forms` updated to include `linked-via`. - `parse_error_unknown_head_surfaces_note` updated for the regenerated hint format (anchor + representative selection). Acceptance (per the 2026-04-26 triage comment on #190): - [x] `(linked-via "<T>")` returns true iff the artifact has at least one outbound link of type T - [x] `linked-from` / `linked-to` / `linked-by` / `linked-via` documented in `docs/getting-started.md` with one-line semantics and a worked example - [x] Error message in `rivet query --sexpr` lists the full operator family, regenerated from a single source of truth (`HEADS`) - [x] Tests cover present / absent / `(not …)` finds gap - [x] Doctest covers the documented `linked-via` semantics Verification: - `cargo test -p rivet-core --lib` — 932 pass (+6) - `cargo test -p rivet-core --test sexpr_doc_examples` — 11 pass (+2) - `cargo test -p rivet-cli` — pass - `cargo clippy --workspace --all-targets -- -D warnings` — clean - `cargo fmt --all -- --check` — clean - `cargo run --release -p rivet-cli -- validate` — 6 errors / 140 warnings / 0 broken cross-refs (byte-identical to pristine main; the 6 errors live in the spar external fixture) Closes #190 Implements: REQ-007 Verifies: REQ-007
📐 Rivet artifact deltaNo artifact changes in this PR. Code-only changes (renderer, CLI wiring, tests) don't touch the artifact graph. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
⚠️ Performance Alert ⚠️
Possible performance regression was detected for benchmark 'Rivet Criterion Benchmarks'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.20.
| Benchmark suite | Current: 8a4ead7 | Previous: 6dfdca6 | Ratio |
|---|---|---|---|
store_insert/10000 |
16211948 ns/iter (± 833212) |
10777546 ns/iter (± 201358) |
1.50 |
link_graph_build/10000 |
35579185 ns/iter (± 1758762) |
22905310 ns/iter (± 293676) |
1.55 |
validate/10000 |
22584395 ns/iter (± 1292492) |
12167984 ns/iter (± 144437) |
1.86 |
diff/10000 |
9229216 ns/iter (± 164621) |
7561473 ns/iter (± 37002) |
1.22 |
This comment was automatically generated by workflow using github-action-benchmark.
5 tasks
avrabe
added a commit
that referenced
this pull request
May 11, 2026
Three queued feature requests now land: rivet bundle (#266), rivet coverage --matrix (#243), s-expr linked-via operator (#265). Plus externals load their own schemas (#267) and STPA TCL numbering is corrected to ISO 26262-8 (#257). Infrastructure: CI concurrency control across all workflows (#258), migration to self-hosted smithy runners (#262), release-npm trigger fix that retroactively unblocked v0.7.0/v0.8.0 npm publication (#261), weekly dependabot (#216), and the wasmtime 42→43 upgrade that retires the RUSTSEC-2026-0114 suppression introduced in v0.8.0 (#260). #125 (provenance-lifecycle) intentionally deferred — 5-week-old branch with conflicts in heavily-churned files (CLAUDE.md, ci.yml, settings). Needs its own attention session, not safe to autonomously rebase. Refs: FEAT-001 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #190 — the issue reported that the s-expr DSL has no discoverable way to express "this artifact has at least one outbound link of type T."
linked-byalready does that semantically, but the issue documents that authors (the issue author included) read it as inbound and reach for spellings that don't exist (linked-via,out-link,has-link,linked-as). The fourlinked-*operators also have no semantic table in the user-facing docs, so users can't reverse-engineer the right form from the listing.This PR adds the discoverable spelling, documents the family, and regenerates the parse-error hint from the lowerer's source of truth so the next added operator cannot leave the hint stale.
What changed
rivet-core/src/sexpr_eval.rs(linked-via "T")form. Lowers toExpr::LinkedBy(T, Wildcard)so the evaluator +links-countcomplement remain one code path. Arity = 1.HEADSextended with"linked-via".classify_filter_errornow builds the unknown-head hint fromHEADS(sorted) rather than a hand-maintained string. The previous opaquelinked-*is replaced with the explicit family.parse_error_unknown_head_surfaces_noteupdated for the regenerated hint.docs/getting-started.mdlinked-via,linked-by(1- and 2-arg),linked-from(1- and 2-arg),linked-to,links-count— direction, filter axis, truth condition. Plus a mnemonic paragraph.exploits, REQ missing inboundverifies, hazard missing inboundprevents.linked-*cross-reference both extended withlinked-via.rivet-core/tests/sexpr_doc_examples.rs(linked-via "T")≡(linked-by "T" _)and the gap-hunt(and (= type …) (not (linked-via "T"))).docs_listed_predicates_all_parse_as_formsextended with(linked-via "satisfies").Acceptance — per the 2026-04-26 triage comment on #190
(linked-via "<link-type>")returns true iff the artifact has at least one outbound link of that type. Testsfilter_linked_via_outbound_present/filter_linked_via_outbound_absent/filter_linked_via_arityinsexpr_eval.rs;(linked-via "T")≡(linked-by "T" _)proven byfilter_linked_via_equivalent_to_linked_by_wildcardanddocs_example_linked_via_outbound_membership.linked-from,linked-to,linked-by,linked-via) documented indocs/getting-started.mdwith a one-line semantic per operator and a worked example. New "Link predicates: which one do I want?" section + 3 worked gap-hunt examples + mnemonic paragraph.rivet query --sexprlists the full operator family, regenerated from a single source of truth.classify_filter_errornow emitsHEADS.sort_unstable(); pinned by the regression testunknown_head_hint_lists_linked_via.(not (linked-via "X"))finds gaps.filter_linked_via_outbound_present,filter_linked_via_outbound_absent,filter_not_linked_via_finds_gap,docs_example_not_linked_via_finds_gap.docs_listed_predicates_all_parse_as_formsnow exerciseslinked-via; the two new doc-example tests assert(linked-via …)against thegetting-started.mdexamples directly.Implements: REQ-007(CLI/query). Trailer present; alsoVerifies: REQ-007for the test additions.Test plan
cargo test -p rivet-core --lib— 932 pass (+6 sexpr cases)cargo test -p rivet-core --test sexpr_doc_examples— 11 pass (+2 cases)cargo test -p rivet-cli— passcargo clippy --workspace --all-targets -- -D warnings— cleancargo fmt --all -- --check— cleancargo run --release -p rivet-cli -- validate—6 errors / 140 warnings / 0 broken cross-refs— byte-identical to pristinemain; the 6 errors live in thesparexternal fixture and are unaffected.Out of scope
The issue body's section #2 ("
linked-*operator family needs documentation") is fully addressed by the new section ingetting-started.md; the optional split into a dedicateddocs/sexpr.md(mentioned in the triage comment as an alternative) is not done — the existing s-expr filtering section already lives ingetting-started.mdand adding a parallel page would duplicate. Happy to lift it out as a follow-up if reviewers prefer.🤖 Generated with Claude Code — issue-triage agent run 2026-05-09.
Generated by Claude Code